logo
menu

useQuery 구현하기 3탄 | 커스텀 useInfiniteQuery

2023. 10. 09.

  • #리액트

2탄에서 useQuery 까지 구현해봤고 이제 useInfiniteQuery 를 구현해보자! useInfiniteQuery 구현하면서 2탄에서 적용하지 않은 useErrorBoundary 를 구현해보자!
 

🚀 구현한 UI

정상

notion image

로컬 에러

notion image
 

에러 바운더리

notion image
 

👨🏻‍💻 간단한 useInfiniteQuery 구현하기

💡
useInfiniteQuery 기능과 errorBoundary 를 추가했다. 구현 코드는 여기를 참고!

인터페이스

export function useInfiniteQuery<T>( fetcher: (pageIndex: number, perPage: number) => Promise<T>, queryKey: unknown[]; perPage?: number; onSuccess?: (data?: T) => void; onError?: (error?: unknown) => void; useErrorBoundary?: boolean; enabled?: boolean; // suspense?: boolean; // useQuery 에서 구현해서 미구현 }
  • 기존 useQuery 에서 perPageuseErrorBoundary 속성을 제외하곤 같다
  • 첫번째 인자인 fetcher에 인자 pageIndex, perPage 가 추가되었는데 useInfiniteQuery 는 현재 페이지(pageIndex), 페이지당 요청 개수 (perPage) 가 필요하므로 추가하였다.
 

useInfiniteQuery 훅

  • example4/hooks/useInfiniteQuery.ts
import { useCallback, useEffect, useRef, useState } from 'react'; import { Cache } from '../utils/cache'; export function useInfiniteQuery<T>( fetcher: (pageIndex: number, perPage: number) => Promise<T>, { queryKey, ...option }: useInfiniteFetchOption<T>, ) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<Error | null>(null); const { enabled = true, // perPage = 10, useErrorBoundary = false, onSuccess, onError, } = option; const [cache] = useState(new Cache<string, InfiniteCache<T>>()); const page = useRef(1); const 쿼리키 = JSON.stringify(queryKey); const fetch = useCallback(async () => { setIsLoading(true); setError(null); try { const cachedData = cache.get(쿼리키) || { data: [], pageIndex: 1, perPage }; const data = await fetcher.apply(null, [page.current, perPage]); cache.set(쿼리키, { // T 로 [] 로 지정할 수 있는데 이를 T 로 인식하게 했는데 타입에서 [] 로 해야 [...data] 할 수 있어 우선 ts-ignore 추가 // @ts-ignore data: [...cachedData.data, ...data], pageIndex: page.current, perPage, }); page.current++; onSuccess && onSuccess(data); } catch (error) { if (error instanceof Error) { setError(error); } onError && onError(error); } finally { setIsLoading(false); } }, []); useEffect(() => { if (!enabled) return; if (cache.has(쿼리키)) return; fetch(); }, [perPage, 쿼리키]); // ErrorBoundary 처리 if (useErrorBoundary && error) { throw new Error(error.message); } const cachedData = cache.get(쿼리키); return { isLoading: isLoading, data: cachedData?.data || [], error, onNextFetch: fetch, }; } type useInfiniteFetchOption<T> = { queryKey: unknown[]; perPage?: number; onSuccess?: (data?: T) => void; onError?: (error?: unknown) => void; useErrorBoundary?: boolean; enabled?: boolean; suspense?: boolean; }; type InfiniteCache<T> = | { data: T; pageIndex: number; perPage: number; } | undefined;
 

핵심 로직

👉 현재 페이지 값 제어하기

const page = useRef(1);
  • page 를 관리하기 위해 useRef 로 관리하였다
 

👉 페이지네이션 API 처리

  • fetch 함수 정의 및 fetcher 로 전달받은 함수에 내부 인자 전달하기
const fetch = useCallback(async () => { setIsLoading(true); setError(null); try { const cachedData = cache.get(쿼리키) || { data: [], pageIndex: 1, perPage }; const data = await fetcher.apply(null, [page.current, perPage]); cache.set(쿼리키, { // T 로 [] 로 지정할 수 있는데 이를 T 로 인식하게 했는데 타입에서 [] 로 해야 [...data] 할 수 있어 우선 ts-ignore 추가 // @ts-ignore data: [...cachedData.data, ...data], pageIndex: page.current, perPage, }); page.current++; onSuccess && onSuccess(data); } catch (error) { if (error instanceof Error) { setError(error); } onError && onError(error); } finally { setIsLoading(false); } } , []);
  • fetch 함수에서 기존에 있는 데이터를 가져오기 위해 cachedData 로 조회했다.
  • const data = await fetcher.apply(null, [page.current, perPage]); 구문을 보면 apply로 함수를 호출하였다
    • 외부에서 주입받는 fetcher(페이지네이션 api 함수) 의 인자는 내부적으로 계속 달라지는 값을 넣어야 하기에 apply 를 이용해서 주입했다.
      ❓ fetcher(페이지네이션 api 함수) 의 인자는 내부적으로 계속 달라지는 값? 💡 내부에서 현재 페이지인 page 로 관리하기 때문에 fetch 함수가 호출될때 마다 달라진 page 값을 전달해야함!
      ❓ apply 메서드란? 💡  apply 메서드는 함수를 호출하는 함수로 인자로 thisarguments (인자) 를 제공한다. ⇒ 예제에서는 page.current 와 perPage 인자를 전달하기 위해 사용했다.
 

👉 에러바운더리 처리

// ErrorBoundary 처리 if (useErrorBoundary && error) { throw new Error(error.message); }
  • 옵션으로 전달한 useErrorBoundary 가 true 이고 error 가 있다면 Error 를 던져서 처리했다.
 
이로써 useInfiniteQuery 를 아주 간단하게 알아봤다!
추가로 정상적인 부분 뿐만 아니라 로컬 에러, 에러바운더리 부분을 제어해서 확인하고 싶다면 주변에 GYU-TEST 키워드로 남겨났다.
해당 글이 도움이 되었으면 좋겠다!!
 

참고